// Copyright (c) 2022, 2023, Oracle and/or its affiliates.

//-----------------------------------------------------------------------------
//
// This software is dual-licensed to you under the Universal Permissive License
// (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License
// 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose
// either license.
//
// If you elect to accept the software under the Apache License, Version 2.0,
// the following applies:
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
//-----------------------------------------------------------------------------

'use strict';

const { Buffer } = require('buffer');
const utils = require("../utils");
const constants = require("../constants.js");
const Message = require("./base.js");
const ThinDbObjectImpl = require("../../dbObject.js");
const ThinLobImpl = require("../../lob.js");
const errors = require('../../../errors');
const types = require('../../../types.js');

/**
 * Handles data like row header, rowdata , ... recevied from an RPC Execute
 *
 * @class MessageWithData
 * @extends {Message}
 */
class MessageWithData extends Message {
  constructor(connection, statement = null, options = null) {
    super(connection);
    this.statement = statement;
    this.options = options;
    this.offset = 0;
    this.numExecs = 1;
    this.arrayDmlRowCounts = false;
    this.requiresDefine = false;
    this.rowIndex = statement.bufferRowCount || 0;
    this.dmlRowCounts = [];
    this.batchErrors = false;
    this.outVariables = [];
    this.inFetch = false;
    this.parseOnly = false;
    this.resultSetsToSetup = [];
    this.deferredErr = null;
  }

  /**
    * processMessage() - Process the data type message
    */
  processMessage(buf, messageType) {
    if (messageType === constants.TNS_MSG_TYPE_DESCRIBE_INFO) {
      buf.skipBytesChunked();
      const prevQueryVars = this.statement.queryVars;
      this.statement.queryVars = [];
      this.statement.numQueryVars = 0;
      this.statement.bufferRowCount = 0;
      this.statement.bufferRowIndex = 0;
      this.processDescribeInfo(buf, this.resultSet, prevQueryVars);
      this.outVariables = this.statement.queryVars;
    } else if (messageType === constants.TNS_MSG_TYPE_ROW_HEADER) {
      this.processRowHeader(buf);
    } else if (messageType === constants.TNS_MSG_TYPE_ROW_DATA) {
      this.processRowData(buf);
    } else if (messageType === constants.TNS_MSG_TYPE_IMPLICIT_RESULTSET) {
      this.processImplicitResultSet(buf);
    } else if (messageType === constants.TNS_MSG_TYPE_BIT_VECTOR) {
      this.processBitVector(buf);
    } else if (messageType === constants.TNS_MSG_TYPE_IO_VECTOR) {
      this.processIOVector(buf);
    } else if (messageType === constants.TNS_MSG_TYPE_FLUSH_OUT_BINDS) {
      this.flushOutBinds = true;
    } else if (messageType === constants.TNS_MSG_TYPE_ERROR) {
      this.processErrorInfo(buf);
    } else {
      super.processMessage(buf, messageType);
    }
  }

  hasMoreData() {
    return !this.processedError && !this.flushOutBinds;
  }

  processErrorInfo(buf) {
    super.processErrorInfo(buf);
    if (this.errorInfo.cursorId !== 0) {
      this.statement.cursorId = this.errorInfo.cursorId;
    }
    if (!this.statement.isPlSql) {
      this.statement.rowCount = this.errorInfo.rowCount;
    }
    // we do not set the lastRowid if the rows affected is 0
    if (this.errorInfo.rowCount > 0) {
      this.statement.lastRowid = utils.encodeRowID(this.errorInfo.rowID);
    }
    this.options.batchErrors = this.errorInfo.batchErrors;
    if (this.batchErrors && this.options.batchErrors === null) {
      this.options.batchErrors = [];
    }
    if (this.errorInfo.num === constants.TNS_ERR_NO_DATA_FOUND && this.statement.isQuery) {
      this.errorInfo.num = 0;
      this.errorOccurred = false;
      this.statement.moreRowsToFetch = false;
    } else if (this.errorInfo.num !== 0 && this.errorInfo.cursorId !== 0) {
      this.connection.statementCache.delete(this.statement.sql);
      this.statement.returnToCache = false;
    }
    if (this.errorInfo.batchErrors) {
      this.errorOccurred = false;
    }
  }

  //---------------------------------------------------------------------------
  // If we have fetched this column earlier, we set that
  // fetch type for the describe info variable received
  // assuming the returned column order is same as previous.
  //---------------------------------------------------------------------------
  _adjustFetchType(pVar, cVar) {
    if ((cVar.fetchInfo.dbType._oraTypeNum === constants.TNS_DATA_TYPE_CLOB
       && pVar.fetchInfo.fetchType._oraTypeNum === constants.TNS_DATA_TYPE_LONG)
        || (cVar.fetchInfo.dbType._oraTypeNum === constants.TNS_DATA_TYPE_BLOB
          && pVar.fetchInfo.fetchType._oraTypeNum === constants.TNS_DATA_TYPE_LONG_RAW)) {
      cVar.type = pVar.fetchInfo.fetchType;
      cVar.maxSize = pVar.maxSize;
    }
  }

  processDescribeInfo(buf, resultSet, prevQueryVars) {
    const statement = resultSet.statement;
    buf.skipUB4();                              // max row size
    statement.numQueryVars = buf.readUB4();
    if (statement.numQueryVars > 0) {
      buf.skipUB1();
    }
    resultSet.metadata = [];
    for (let i = 0; i < statement.numQueryVars; i++) {
      const variable = this.processColumnInfo(buf, i + 1);
      if (prevQueryVars && i < prevQueryVars.length) {
        this._adjustFetchType(prevQueryVars[i], variable);
      }
      statement.queryVars.push(variable);
      resultSet.metadata.push(variable.fetchInfo);
    }

    let numBytes = buf.readUB4();
    if (numBytes > 0) {
      buf.skipBytesChunked();                   // current date
    }
    buf.skipUB4();                              // dcbflag
    buf.skipUB4();                              // dcbmdbz
    buf.skipUB4();                              // dcbmnpr
    buf.skipUB4();                              // dcbmxpr
    numBytes = buf.readUB4();
    if (numBytes > 0) {
      buf.skipBytesChunked();
    }

    this.resultSetsToSetup.push(resultSet);
  }

  processColumnInfo(buf, columnNum) {
    const dataType = buf.readUInt8();
    buf.skipUB1(); // flags
    const precision = buf.readInt8();
    const scale = buf.readInt8();
    const maxSize = buf.readUB4();
    buf.skipUB4();                              // max number of array elements
    buf.skipUB8();                              // cont flags
    let oid;
    let numBytes = buf.readUB4();               // OID
    if (numBytes > 0) {
      oid = Buffer.from(buf.readBytesWithLength());
    }
    buf.skipUB2();                              // version
    buf.skipUB2();                              // character set id
    const csfrm = buf.readUInt8();              // character set form
    let size = buf.readUB4();
    if (dataType === constants.TNS_DATA_TYPE_RAW) {
      size = maxSize;
    }
    if (buf.caps.ttcFieldVersion >= constants.TNS_CCAP_FIELD_VERSION_12_2) {
      buf.skipUB4();                            // oaccolid
    }
    const nullable = Boolean(buf.readUInt8());
    buf.skipUB1();                              // v7 length of name
    let name;
    numBytes = buf.readUB4();
    if (numBytes > 0) {
      name = buf.readStr(constants.CSFRM_IMPLICIT);
    }
    let schema;
    numBytes = buf.readUB4();
    if (numBytes > 0) {
      schema = buf.readStr(constants.CSFRM_IMPLICIT);
    }
    numBytes = buf.readUB4();
    let typeName;
    if (numBytes > 0) {
      typeName = buf.readStr(constants.CSFRM_IMPLICIT);
    }
    buf.skipUB2();                              // column position
    const udsFlags = buf.readUB4();             // uds flag

    // build metadata
    const fetchInfo = {
      name: name,
      dbType: types.getTypeByOraTypeNum(dataType, csfrm),
      nullable: nullable
    };
    fetchInfo.isJson = Boolean(udsFlags & constants.TNS_UDS_FLAGS_IS_JSON);
    if (buf.caps.ttcFieldVersion >= constants.TNS_CCAP_FIELD_VERSION_23_1) {
      numBytes = buf.readUB4();
      if (numBytes > 0) {
        fetchInfo.domainSchema = buf.readStr(constants.CSFRM_IMPLICIT);
      }
      numBytes = buf.readUB4();
      if (numBytes > 0) {
        fetchInfo.domainName = buf.readStr(constants.CSFRM_IMPLICIT);
      }
    }
    if (buf.caps.ttcFieldVersion >= constants.TNS_CCAP_FIELD_VERSION_23_1_EXT_3) {
      if (buf.readUB4() > 0) {
        fetchInfo.annotations = {};
        buf.skipUB1();
        const numAnnotations = buf.readUB4();
        buf.skipUB1();
        let key, value;
        for (let i = 0; i < numAnnotations; i++) {
          buf.skipUB4();
          value = "";
          key = buf.readStr(constants.CSFRM_IMPLICIT);
          numBytes = buf.readUB4();
          if (numBytes > 0) {
            value = buf.readStr(constants.CSFRM_IMPLICIT);
          }
          fetchInfo.annotations[key] = value;
          buf.skipUB4();                        // flags
        }
        buf.skipUB4();                          // flags
      }
    }

    switch (fetchInfo.dbType) {
      case types.DB_TYPE_VARCHAR:
      case types.DB_TYPE_NVARCHAR:
      case types.DB_TYPE_CHAR:
      case types.DB_TYPE_NCHAR:
      case types.DB_TYPE_RAW:
        fetchInfo.byteSize = size;
        break;
      case types.DB_TYPE_NUMBER:
        fetchInfo.precision = precision;
        break;
      case types.DB_TYPE_TIMESTAMP:
      case types.DB_TYPE_TIMESTAMP_TZ:
      case types.DB_TYPE_TIMESTAMP_LTZ:
        fetchInfo.precision = scale;
        break;
      case types.DB_TYPE_OBJECT:
        fetchInfo.dbTypeClass = this.connection._getDbObjectType(schema,
          typeName, undefined, oid);
        if (fetchInfo.dbTypeClass.partial) {
          this.connection._partialDbObjectTypes.push(fetchInfo.dbTypeClass);
        }
        break;
      default:
        break;
    }
    if (fetchInfo.dbType === types.DB_TYPE_NUMBER) {
      fetchInfo.scale = scale;
    }
    return {
      fetchInfo: fetchInfo,
      type: fetchInfo.dbType,
      maxSize: maxSize,
      columnNum: columnNum,
      values: new Array(this.options.fetchArraySize)
    };
  }

  processRowHeader(buf) {
    buf.skipUB1();                              // flags
    buf.skipUB2();                              // num requests
    buf.skipUB4();                              // iteration number
    buf.skipUB4();                              // num iters
    buf.skipUB2();                              // buffer length
    let numBytes = buf.readUB4();
    if (numBytes > 0) {
      this.bitVector = Buffer.from(buf.readBytesWithLength());
    }
    numBytes = buf.readUB4();
    if (numBytes > 0) {
      buf.skipBytesChunked();                   // rxhrid
    }
  }

  isDuplicateData(columnName) {
    if (!this.bitVector) {
      return false;
    }
    const byteNum = Math.floor(columnName / 8);
    const bitNum = columnName % 8;
    return (this.bitVector[byteNum] & (1 << bitNum)) === 0;
  }

  processRowData(buf) {
    let value;
    for (const [col, variable] of this.outVariables.entries()) {
      if (variable.isArray) {
        variable.numElementsInArray = buf.readUB4();
        const values = new Array(variable.numElementsInArray).fill(null);
        for (let pos = 0; pos < variable.numElementsInArray; pos++) {
          value = this.processColumnData(buf, variable, pos);
          values[pos] = value;
        }
        variable.values[this.rowIndex] = values;
      } else if (this.statement.isReturning) {
        const numRows = buf.readUB4();
        const values = Array(numRows).fill(null);
        for (let j = 0; j < numRows; j++) {
          values[j] = this.processColumnData(buf, variable, j);
        }
        variable.values[this.rowIndex] = values;
      } else if (this.isDuplicateData(col)) {
        if (this.rowIndex === 0 && variable.outConverter) {
          value = variable.lastRawValue;
        } else {
          value = variable.values[this.statement.lastRowIndex];
        }
        variable.values[this.rowIndex] = value;
      } else {
        value = this.processColumnData(buf, variable, this.rowIndex);
        variable.values[this.rowIndex] = value;
      }
    }
    this.rowIndex++;
    if (this.inFetch) {
      this.statement.lastRowIndex = this.rowIndex - 1;
      this.statement.bufferRowCount++;
      this.bitVector = null;
    }
  }

  processIOVector(buf) {
    let numBytes;
    buf.skipUB1();                              // flag
    const temp16 = buf.readUB2();              // num requests
    const temp32 = buf.readUB4();              // iter num
    const numBinds = temp32 * 256 + temp16;
    buf.skipUB4();                              // num iters this time
    buf.skipUB2();                              // uac buffer length
    numBytes = buf.readUB2();                   // bit vector for fast fetch
    if (numBytes > 0) {
      buf.skipBytes(numBytes);
    }
    numBytes = buf.readUB2();                   // rowid
    if (numBytes > 0) {
      buf.skipBytes(numBytes);
    }
    this.outVariables = [];
    for (let i = 0; i < numBinds; i++) {              // bind directions
      const bindInfo = this.statement.bindInfoList[i];
      bindInfo.bindDir = buf.readUInt8();
      if (bindInfo.bindDir === constants.TNS_BIND_DIR_INPUT) {
        continue;
      }
      this.outVariables.push(bindInfo.bindVar);
    }
  }

  processColumnData(buf, variable) {
    const dbType = variable.type;
    const oraTypeNum = dbType._oraTypeNum;
    const csfrm = dbType._csfrm;
    const maxSize = variable.maxSize;

    let colValue = null;
    if (maxSize === 0 && oraTypeNum !== constants.TNS_DATA_TYPE_LONG
      && oraTypeNum !== constants.TNS_DATA_TYPE_LONG_RAW
      && oraTypeNum !== constants.TNS_DATA_TYPE_UROWID) {
      colValue = null;
    } else if (
      oraTypeNum === constants.TNS_DATA_TYPE_VARCHAR ||
      oraTypeNum === constants.TNS_DATA_TYPE_CHAR ||
      oraTypeNum === constants.TNS_DATA_TYPE_LONG
    ) {
      if (csfrm === constants.CSFRM_NCHAR) {
        buf.caps.checkNCharsetId();
      }
      colValue = buf.readStr(csfrm);
    } else if (oraTypeNum === constants.TNS_DATA_TYPE_RAW ||
      oraTypeNum === constants.TNS_DATA_TYPE_LONG_RAW) {
      colValue = buf.readBytesWithLength();
      if (colValue !== null) {
        colValue = Buffer.from(colValue);
      }
    } else if (oraTypeNum === constants.TNS_DATA_TYPE_NUMBER) {
      colValue = buf.readOracleNumber();
      if (!this.inFetch && colValue !== null)
        colValue = parseFloat(colValue);
    } else if (
      oraTypeNum === constants.TNS_DATA_TYPE_DATE ||
      oraTypeNum === constants.TNS_DATA_TYPE_TIMESTAMP ||
      oraTypeNum === constants.TNS_DATA_TYPE_TIMESTAMP_LTZ ||
      oraTypeNum === constants.TNS_DATA_TYPE_TIMESTAMP_TZ
    ) {
      const useLocalTime = (oraTypeNum === constants.TNS_DATA_TYPE_DATE ||
        oraTypeNum === constants.TNS_DATA_TYPE_TIMESTAMP);
      colValue = buf.readOracleDate(useLocalTime);
    } else if (oraTypeNum === constants.TNS_DATA_TYPE_ROWID) {
      if (!this.inFetch) {
        colValue = buf.readStr(constants.CSFRM_IMPLICIT);
      } else {
        const numBytes = buf.readUInt8();
        if (isNullLength(numBytes)) {
          colValue = null;
        } else {
          const rowid = buf.readRowID();
          colValue = utils.encodeRowID(rowid);
        }
      }
    } else if (oraTypeNum === constants.TNS_DATA_TYPE_UROWID) {
      if (!this.inFetch) {
        colValue = buf.readStr(constants.CSFRM_IMPLICIT);
      } else {
        colValue = buf.readURowID();
      }
    } else if (oraTypeNum === constants.TNS_DATA_TYPE_BINARY_DOUBLE) {
      colValue = buf.readBinaryDouble();
    } else if (oraTypeNum === constants.TNS_DATA_TYPE_BINARY_FLOAT) {
      colValue = buf.readBinaryFloat();
    } else if (oraTypeNum === constants.TNS_DATA_TYPE_BINARY_INTEGER) {
      colValue = buf.readOracleNumber();
      if (colValue !== null)
        colValue = parseFloat(colValue);
    } else if (oraTypeNum === constants.TNS_DATA_TYPE_CURSOR) {
      const numBytes = buf.readUInt8();
      if (isNullLength(numBytes)) {
        colValue = null;
      } else {
        colValue = this.createCursorFromDescribe(buf);
        colValue.statement.cursorId = buf.readUB2();
        // If the cursor ID is 0 for the returned ref cursor then
        // it is an invalid cursor
        if (colValue.statement.cursorId === 0 && variable.dir !== constants.BIND_IN) {
          if (this.options.nullifyInvalidCursor) {
            colValue = null;
          } else {
            errors.throwErr(errors.ERR_INVALID_REF_CURSOR);
          }
        }
      }
    } else if (oraTypeNum === constants.TNS_DATA_TYPE_BOOLEAN) {
      colValue = buf.readBool();
    } else if (oraTypeNum === constants.TNS_DATA_TYPE_CLOB || oraTypeNum === constants.TNS_DATA_TYPE_BLOB) {
      const bvalue = buf.readUB4();
      if (bvalue > 0) { // Non Null data in column
        colValue = new ThinLobImpl();
        const length = buf.readUB8();
        const chunkSize = buf.readUB4();
        const locator = Buffer.from(buf.readBytesWithLength());
        colValue.init(this.connection, locator, dbType, length, chunkSize);
      }
    } else if (oraTypeNum === constants.TNS_DATA_TYPE_JSON) {
      colValue = buf.readOson();
    } else if (oraTypeNum === constants.TNS_DATA_TYPE_INT_NAMED) {
      const obj = buf.readDbObject();
      if (obj.packedData) {
        const objType = (variable.fetchInfo) ? variable.fetchInfo.dbTypeClass :
          variable.typeClass;
        colValue = new ThinDbObjectImpl(objType, obj.packedData);
        colValue.toid = obj.toid;
        colValue.oid = obj.oid;
      }
    } else {
      errors.throwErr(errors.ERR_UNSUPPORTED_DATA_TYPE, dbType.num,
        variable.columnNum);
    }

    if (!this.inFetch) {
      const actualNumBytes = buf.readSB4();
      if (actualNumBytes < 0 && oraTypeNum === constants.TNS_DATA_TYPE_BOOLEAN) {
        colValue = null;

      // For objects, maxsize validation is skipped
      } else if (actualNumBytes !== 0 && colValue !== null &&
        oraTypeNum !== constants.TNS_DATA_TYPE_INT_NAMED) {
        this.saveDeferredErr(errors.ERR_INSUFFICIENT_BUFFER_FOR_BINDS);
      }
    } else if (oraTypeNum === constants.TNS_DATA_TYPE_LONG || oraTypeNum === constants.TNS_DATA_TYPE_LONG_RAW || variable.maxSize > buf.caps.maxStringSize) {
      buf.skipSB4();                            // null indicator
      buf.skipUB4();                            // return code
    }
    return colValue;
  }

  processReturnParameter(buf) {
    let keywordNum = 0;
    let keyTextValue;
    let numParams = buf.readUB2();              // al8o4l (ignored)

    for (let i = 0; i < numParams; i++) {
      buf.skipUB4();
    }
    let numBytes = buf.readUB2();               // al8txl (ignored)
    if (numBytes > 0) {
      buf.skipBytes(numBytes);
    }
    numParams = buf.readUB2();                  // num key/value pairs
    for (let i = 0; i < numParams; i++) {
      numBytes = buf.readUB2();                 // key
      if (numBytes > 0) {
        keyTextValue = buf.readStr(constants.CSFRM_IMPLICIT);
      }
      numBytes = buf.readUB2();                 // value
      if (numBytes > 0) {
        buf.skipBytesChunked();
      }
      keywordNum = buf.readUB2();               // keyword num
      if (keywordNum === constants.TNS_KEYWORD_NUM_CURRENT_SCHEMA) {
        this.connection.currentSchema = keyTextValue;
      } else if (keywordNum === constants.TNS_KEYWORD_NUM_EDITION) {
        this.connection._edition = keyTextValue;
      }
    }
    numBytes = buf.readUB2();                   // registration
    if (numBytes > 0) {
      buf.skip(numBytes);
    }
    if (this.arrayDmlRowCounts) {
      const numRows = buf.readUB4();
      const rowCounts = this.options.dmlRowCounts = [];
      for (let i = 0; i < numRows; i++) {
        const rowCount = buf.readUB8();
        rowCounts.push(rowCount);
      }
    }
  }

  async postProcess() {
    if (this.deferredErr) {
      throw this.deferredErr;
    }

    if (this.outVariables) {
      for (const variable of this.outVariables) {
        if (variable.isArray) {
          if (variable.outConverter) {
            for (let pos = 0; pos < variable.numElementsInArray; pos++) {
              variable.values[0][pos] = await variable.outConverter(variable.values[0][pos]);
            }
          }
        } else {
          if (variable.outConverter) {
            variable.values[0] = await variable.outConverter(variable.values[0]);
          }
        }
      }
    }
    await this.connection._populatePartialDbObjectTypes();
    for (const resultSet of this.resultSetsToSetup) {
      resultSet._setup(this.options, resultSet.metadata);
      // LOBs always require define and they change the type that is actually
      // returned by the server
      for (const variable of resultSet.statement.queryVars) {
        if (variable.type === types.DB_TYPE_CLOB ||
            variable.type === types.DB_TYPE_NCLOB ||
            variable.type === types.DB_TYPE_BLOB ||
            variable.type === types.DB_TYPE_JSON) {
          if (variable.type !== variable.fetchInfo.fetchType) {
            variable.type = variable.fetchInfo.fetchType;
            variable.maxSize = constants.TNS_MAX_LONG_LENGTH;
          }
          if (!resultSet.statement.noPrefetch) {
            resultSet.statement.requiresDefine = true;
            resultSet.statement.noPrefetch = true;
          }
        }
      }
    }
  }

  preProcess() {
    if (this.statement.isReturning && !this.parseOnly) {
      this.outVariables = [];
      for (const bindInfo of this.statement.bindInfoList) {
        if (bindInfo.isReturnBind) {
          this.outVariables.push(bindInfo.bindVar);
        }
      }
    }

    if (this.statement.isQuery) {
      this.inFetch = true;
      if (this.statement.queryVars) {
        this.outVariables = [];
        for (let i = 0; i < this.statement.queryVars.length; i++) {
          this.outVariables.push(this.statement.queryVars[i]);
        }
      }
    }
  }

  processBitVector(buf) {
    this.numColumnsSent = buf.readUB2();
    let numBytes = Math.floor(this.statement.numQueryVars / 8);
    if (this.statement.numQueryVars % 8 > 0) {
      numBytes += 1;
    }
    this.bitVector = Buffer.from(buf.readBytes(numBytes));
  }

  processBindParams(buf, params) {
    const bindVars = [];
    const nonReturningParams = [];
    for (const bindInfo of params) {
      if (!bindInfo.isReturnBind) {
        nonReturningParams.push(bindInfo);
      }
      bindVars.push(bindInfo.bindVar);
    }
    this.writeColumnMetadata(buf, bindVars);
    return nonReturningParams;
  }

  writeColumnMetadata(buf, bindVars) {
    for (const variable of bindVars) {
      let oraTypeNum = variable.type._oraTypeNum;
      let maxSize = variable.maxSize || variable.type._bufferSizeFactor;
      let lobPrefetchLength = 0;

      // NCHAR, NVARCHAR reports ORA-01460: unimplemented or unreasonable
      // conversion requested if maxSize is not multiplied by the
      // bufferSizeFactor
      if (variable.type._csfrm === constants.CSFRM_NCHAR) {
        maxSize *= variable.type._bufferSizeFactor;
      }
      if ([constants.TNS_DATA_TYPE_ROWID, constants.TNS_DATA_TYPE_UROWID].includes(oraTypeNum)) {
        oraTypeNum = constants.TNS_DATA_TYPE_VARCHAR;
        maxSize = constants.TNS_MAX_UROWID_LENGTH;
      }
      let flag = constants.TNS_BIND_USE_INDICATORS;
      if (variable.isArray) {
        flag |= constants.TNS_BIND_ARRAY;
      }
      let contFlag = 0;
      if (variable.type === types.DB_TYPE_BLOB ||
          variable.type === types.DB_TYPE_CLOB ||
          variable.type === types.DB_TYPE_NCLOB) {
        contFlag = constants.TNS_LOB_PREFETCH_FLAG;
      } else if (variable.type === types.DB_TYPE_JSON) {
        contFlag = constants.TNS_LOB_PREFETCH_FLAG;
        maxSize = lobPrefetchLength = constants.TNS_JSON_MAX_LENGTH;
      }
      buf.writeUInt8(oraTypeNum);
      buf.writeUInt8(flag);
      // precision and scale are always written as zero as the server
      // expects that and complains if any other value is sent!
      buf.writeUInt8(0);
      buf.writeUInt8(0);
      if (maxSize > buf.caps.maxStringSize) {
        buf.writeUB4(constants.TNS_MAX_LONG_LENGTH);
      } else {
        buf.writeUB4(maxSize);
      }

      if (variable.isArray) {
        buf.writeUB4(variable.maxArraySize);
      } else {
        buf.writeUB4(0);                        // max num elements
      }
      buf.writeUB4(contFlag);
      if (variable.objType) {
        const objType = variable.objType;
        buf.writeUB4(objType.oid.length);
        buf.writeBytesWithLength(objType.oid);
        buf.writeUB2(objType.version);
      } else {
        buf.writeUB4(0);                        // OID
        buf.writeUB2(0);                        // version
      }
      if (variable.type._csfrm !== 0) {
        buf.writeUB2(constants.TNS_CHARSET_UTF8);
      } else {
        buf.writeUB2(0);
      }
      buf.writeUInt8(variable.type._csfrm);
      buf.writeUB4(lobPrefetchLength);          // max chars (LOB prefetch)
      if (buf.caps.ttcFieldVersion >= constants.TNS_CCAP_FIELD_VERSION_12_2) {
        buf.writeUB4(0);                        // oaccolid
      }
    }
  }

  writeBindParamsRow(buf, params, pos) {
    const offset = this.offset;
    let foundLong = false;
    for (const bindInfo of params) {
      if (bindInfo.isReturnBind)
        continue;
      const variable = bindInfo.bindVar;
      if (variable.isArray) {
        const numElements = variable.values.length;
        buf.writeUB4(numElements);
        for (let i = 0; i < numElements; i++) {
          this.writeBindParamsColumn(buf, variable, variable.values[i]);
        }
      } else {
        if ((!this.statement.isPlSql) && variable.maxSize > buf.caps.maxStringSize) {
          foundLong = true;
        } else {
          this.writeBindParamsColumn(buf, variable,
            variable.values[pos + offset]);
        }
      }
    }
    if (foundLong) {
      for (const bindInfo of params) {
        if (bindInfo.isReturnBind)
          continue;
        const variable = bindInfo.bindVar;
        if (variable.maxSize > buf.caps.maxStringSize) {
          this.writeBindParamsColumn(buf, variable, variable.values[pos + offset]);
        }
      }
    }
  }

  writeBindParamsColumn(buf, variable, value) {
    const oraTypeNum = variable.type._oraTypeNum;
    let tempVal;
    if ((value === undefined || value === null) && oraTypeNum !== constants.TNS_DATA_TYPE_CURSOR && oraTypeNum !== constants.TNS_DATA_TYPE_JSON) {
      if (oraTypeNum === constants.TNS_DATA_TYPE_BOOLEAN) {
        buf.writeUInt8(constants.TNS_ESCAPE_CHAR);
        buf.writeUInt8(1);
      } else if (oraTypeNum === constants.TNS_DATA_TYPE_INT_NAMED) {
        buf.writeUB4(0);                // TOID
        buf.writeUB4(0);                // OID
        buf.writeUB4(0);                // snapshot
        buf.writeUB4(0);                // version
        buf.writeUB4(0);                // packed data length
        buf.writeUB4(constants.TNS_OBJ_TOP_LEVEL);    // flags
      } else {
        buf.writeUInt8(0);
      }
    } else if (oraTypeNum === constants.TNS_DATA_TYPE_NUMBER ||
      oraTypeNum === constants.TNS_DATA_TYPE_BINARY_INTEGER) {
      if (typeof value === 'boolean') {
        tempVal = (value) ? "1" : "0";
      } else {
        tempVal = value.toString();
      }
      buf.writeOracleNumber(tempVal);
    } else if (oraTypeNum === constants.TNS_DATA_TYPE_VARCHAR ||
      oraTypeNum === constants.TNS_DATA_TYPE_CHAR ||
      oraTypeNum === constants.TNS_DATA_TYPE_LONG ||
      oraTypeNum === constants.TNS_DATA_TYPE_RAW ||
      oraTypeNum === constants.TNS_DATA_TYPE_LONG_RAW) {
      if (variable.type._csfrm === constants.CSFRM_NCHAR) {
        buf.caps.checkNCharsetId();
        value = Buffer.from(value, constants.TNS_ENCODING_UTF16).swap16();
      } else {
        value = Buffer.from(value);
      }
      buf.writeBytesWithLength(value);
    } else if (
      oraTypeNum === constants.TNS_DATA_TYPE_DATE ||
      oraTypeNum === constants.TNS_DATA_TYPE_TIMESTAMP ||
      oraTypeNum === constants.TNS_DATA_TYPE_TIMESTAMP_TZ ||
      oraTypeNum === constants.TNS_DATA_TYPE_TIMESTAMP_LTZ
    ) {
      buf.writeOracleDate(value, variable.type);
    } else if (oraTypeNum === constants.TNS_DATA_TYPE_BINARY_DOUBLE) {
      buf.writeBinaryDouble(value);
    } else if (oraTypeNum === constants.TNS_DATA_TYPE_BINARY_FLOAT) {
      buf.writeBinaryFloat(value);
    } else if (oraTypeNum === constants.TNS_DATA_TYPE_CURSOR) {
      let cursor = value;
      if (!value) {
        cursor = this.connection._createResultSet();
      }
      if (cursor.statement.cursorId === 0) {
        buf.writeUInt8(1);
        buf.writeUInt8(0);
      } else {
        buf.writeUB4(1);
        buf.writeUB4(cursor.statement.cursorId);
      }
    } else if (oraTypeNum === constants.TNS_DATA_TYPE_BOOLEAN) {
      if (value) {
        buf.writeUInt8(2);
        buf.writeUInt16BE(0x0101);
      } else {
        buf.writeUInt16BE(0x0100);
      }
    } else if (oraTypeNum === constants.TNS_DATA_TYPE_CLOB || oraTypeNum === constants.TNS_DATA_TYPE_BLOB) {
      buf.writeUB4(value._locator.length);
      buf.writeBytesWithLength(value._locator);
    } else if ([constants.TNS_DATA_TYPE_ROWID, constants.TNS_DATA_TYPE_UROWID].includes(oraTypeNum)) {
      buf.writeBytesWithLength(Buffer.from(value));
    } else if (oraTypeNum === constants.TNS_DATA_TYPE_JSON) {
      buf.writeOson(value);
    } else if (oraTypeNum === constants.TNS_DATA_TYPE_INT_NAMED) {
      buf.writeDbObject(value);
    } else {
      const message = `Binding data of type ${variable.type}`;
      errors.throwErr(errors.ERR_NOT_IMPLEMENTED, message);
    }
  }

  createCursorFromDescribe(buf) {
    const resultSet = this.connection._createResultSet(this.options);
    resultSet.options.moreRowsToFetch = true;
    resultSet.statement.isQuery = true;
    resultSet.statement.requiresFullExecute = true;
    this.processDescribeInfo(buf, resultSet);
    return resultSet;
  }

  processImplicitResultSet(buf) {
    this.options.implicitResultSet = [];
    const numResults = buf.readUB4();
    for (let i = 0; i < numResults; i++) {
      const numBytes = buf.readUInt8();
      buf.skipBytes(numBytes);
      const childResultSet = this.createCursorFromDescribe(buf);
      childResultSet.statement.cursorId = buf.readUB2();
      this.options.implicitResultSet.push(childResultSet);
    }
  }
}

const isNullLength = (len) => {
  return len === 0 || len === constants.TNS_NULL_LENGTH_INDICATOR;
};

module.exports = MessageWithData;
